/*
 * Copyright (c) 2011 Obix Labs Limited
 * Redistribution and use in source and binary forms, 
 * with or without modification, are permitted provided 
 * that the following conditions are met:
 * 
 * 		Redistribution of source code must retain the above 
 * 		copyright notice, this list of conditions and the 
 * 		following disclaimer.
 *
 * 		Redistribution in binary form must reproduce the 
 * 		above copyright notice, this list of conditions 
 * 		and the following disclaimer in the documentation 
 * 		and/or other materials provided with the distribution.
 * 
 * 	THIS SOFTWARE IS PROVIDED "AS IS," WITHOUT A WARRANTY OF 
 * 	ANY KIND. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS 
 * 	AND WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, 
 * 	FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, 
 * 	ARE HEREBY EXCLUDED. OBIX LABS LIMITED ("Obix Labs") AND ITS 
 * 	LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE 
 * 	AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR 
 * 	ITS DERIVATIVES. IN NO EVENT WILL Obix Labs OR ITS LICENSORS BE 
 * 	LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, 
 * 	INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE 
 * 	DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF 
 * 	LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS 
 * 	SOFTWARE, EVEN IF Obix Labs HAS BEEN ADVISED OF THE POSSIBILITY OF 
 * 	SUCH DAMAGES.
 */
package com.obixlabs.commons.testutil;

import static com.obixlabs.commons.io.IOResourceCloser.close;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.RandomStringUtils;

import com.obixlabs.commons.util.RandomCharacterGenerator;

/*
 * Utility class for testing default behaviour 
 * and functionality of POJO Beans. 
 */
public class BeanTestUtil
{
	
	public static final int DEFAULT_TEST_STRING_LENGTH = 25;
	
	public static final int DEFAULT_TEST_COLLECTION_SIZE = 5;
	
	private static final Random randomNumberGenerator = 
													new Random(System.currentTimeMillis());
	private static final RandomCharacterGenerator textGenerator = 
													new RandomCharacterGenerator(
																randomNumberGenerator);

	public synchronized static Set<String> createTestStringSet()
	{
		Set<String> result = new HashSet<String>();
		int maxTestStringSize = randomNonZeroInt(DEFAULT_TEST_STRING_LENGTH);		
		for (int i=0;i<randomNonZeroInt(DEFAULT_TEST_COLLECTION_SIZE);i++)
			result.add(createRandomAlphaNumericString(maxTestStringSize));		
		return result;
	}	
		
	public synchronized static Collection<String> createTestStringCollection()
	{ return createTestStringSet(); }	
	
	/*
	 * Create collection of test strings. 
	 * 
	 * @param size	The size of the collection to create i.e. 
	 * the number of test elements in the collection.
	 * @param maxStringLength	The maximum size of the generated strings. 
	 * @return	A collection of test strings of the given size.
	 */
	public synchronized static List<String> createTestStringCollection(
																							int size, int maxStringLength)
	{
		List<String> result = new ArrayList<String>();
		int maxTestStringSize = randomNonZeroInt(maxStringLength);		
		for (int i=0;i<size;i++)
			result.add(createRandomAlphaNumericString(maxTestStringSize));		
		return result;
	}	
	
	/*
	 * Utility method which creates a random string no longer than the 
	 * given length. This is intended as a convenience method 
	 * for generating test data. 
	 * 
	 * @param maxLength The maximum length of the string 
	 * to create.
	 */
	public synchronized static String 
									createRandomString(
													int maxLength)
	{
		String result;
		result=textGenerator.string(maxLength);		
		return result;
	}

	/*
	 * Utility method which creates a random alpha-numeric string 
	 * no longer than the given length. This is intended as a 
	 * convenience method for generating test data. 
	 * 
	 * @param maxLength The maximum length of the string 
	 * to create.
	 */	
	public synchronized static String 
								createRandomAlphaNumericString(
																int maxLength)
	{
		String result;
		result=textGenerator.alphaNumericString(maxLength);		
		return result;
	}
	
	public synchronized static String randomAlphaNumericString() {
		String result;
		int size = randomNonZeroInt(DEFAULT_TEST_STRING_LENGTH);
		result = RandomStringUtils.randomAlphanumeric(size);
		return result;
	}	

	public synchronized static String randomAlphabeticString() {
		String result;
		int size = randomNonZeroInt(DEFAULT_TEST_STRING_LENGTH);
		result = RandomStringUtils.randomAlphabetic(size);
		return result;
	}		
	
	public synchronized static String randomNumericString() {
		String result;
		int size = randomNonZeroInt(DEFAULT_TEST_STRING_LENGTH);
		result = RandomStringUtils.randomNumeric(size);
		return result;
	}	
	
	public synchronized static String randomNumericString(
																			int maxLength) {
		String result;
		int size = randomNonZeroInt(maxLength);
		result = RandomStringUtils.randomNumeric(size);
		return result;
	}

    public synchronized static int randomInt() 
    { return randomInt(Integer.MAX_VALUE); }
    

    public synchronized static int randomInt(int max) {
    	int result;    	    	
        result = randomNumberGenerator.nextInt(max);
        return result;
    }	
    
	
	/*
	 * Utility method which generates a positive random 
	 * number no greater than the given value. 
	 * 
	 * @param maxValue	The maximum value of the 
	 * random number to generate.
	 */	
    public synchronized static int randomNonZeroInt(int maxValue)
    {
    	int result;    	    	
        result = randomNumberGenerator.nextInt(maxValue);
        if (result <=0 )
            result = maxValue;
        return result;
    }
    
    public synchronized static boolean randomBoolean()
    {
    	boolean result = false;
    	int base = randomNumberGenerator.nextInt();
        if (base%3==0 ) result = true;
        return result;
    }
    
    public synchronized static <T> T randomElement(T[] objects) {
    	T result;    	
    	int index = randomNonZeroInt(objects.length-1);
    	result = objects[index];    	
    	return result;
    }
    

	/*
	 * Utility method which generates a positive fraction between 0 and .9. 
	 */	
    public synchronized static double randomFraction()
    { return randomNonZeroInt(99)/100; }   
    
	
	/*
	 * Tests that the value of a given property on a bean 
	 * matches an expected value. This method can be used 
	 * to test the effectiveness of bean mutator methods.
	 * 
	 * @param bean	The bean being tested.
	 * @param expectedValue	The expected value of the property.
	 * @param propertyName	The name of the property whose value 
	 * is to be validated.
	 *  
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws NoSuchMethodException
	 */
	public static void testInitialPropertyValue(Object bean,
												Object expectedValue,
												String propertyName)
	{
		try {
			if (expectedValue!=null)
				assertEquals(BeanUtils.getProperty(bean, propertyName), expectedValue);
			else
				assertTrue("Property '" + propertyName + "' failed to return null as expected.", 
								BeanUtils.getProperty(bean, propertyName)==null);
		} catch (Exception exce) {
			throw new RuntimeException(exce);
		}
	}
	
	/*
	 * The same as {@link #testMutatorsAndAccessors(Object, Object, String, boolean)}
	 * except that it does not test acceptance of null property values.
	 *  
	 * @param bean	The bean being tested.
	 * 
	 * @param value	The value with which to test the bean property's 
	 * set and get methods.
	 * 
	 * @param propertyName	The name of the property whose get and set
	 * methods are being tested.
	 * 
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws NoSuchMethodException
	 */	
	public static void testMutatorsAndAccessors(Object bean, Object value,
												String propertyName)
												throws 	IllegalAccessException, 
														InvocationTargetException,
														NoSuchMethodException
	{testMutatorsAndAccessors(bean, value, propertyName,false);}	
	
	/*
	 * Tests a bean property's get and set methods with a
	 * given value. It can also tests that these methods 
	 * accept and return null values.
	 * 
	 * @param bean	The bean whose property get and set methods
	 * are to be tested. 
	 * 
	 * @param value	The value with which to test the get and set 
	 * methods.
	 * 
	 * @param propertyName	The name of the bean property whose
	 * get and set methods are to be tested. 
	 * 
	 * @param testNull	If true, it will also test that the 
	 * property mutator accepts a null value, and, that when 
	 * invoked in that way, the corresponding getter method 
	 * will return null thereafter.
	 * 
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws NoSuchMethodException
	 */
	public static void testMutatorsAndAccessors(Object bean, 
												Object value, 
												String propertyName,
												boolean testNull) 
												throws 	IllegalAccessException, 
														InvocationTargetException, 
														NoSuchMethodException
	{
		BeanUtils.setProperty(bean, propertyName, value);
		assertEquals(BeanUtils.getProperty(bean, propertyName), value);
		
		if (testNull)
		{
			BeanUtils.setProperty(bean, propertyName, null);
			assertNull("Property '" + propertyName + "' failed to return null as expected.", 
						BeanUtils.getProperty(bean, propertyName));			
		}
	}

	/*
	 * Tests if two collections are identical in structure. This is achieved 
	 * by comparing the size of both collections, and where both are equal 
	 * in size, then one of them must also contain all the elements of the other. 
	 * The validity of this test can be shown with a relatively simple 
	 * proof by contradiction.
	 *  
	 * @param expected	The base of the comparison i.e. the expected 
	 * collection.
	 *  
	 * @param actual	The collection to compare to the expected 
	 * collection. 
	 * 
	 * @return	True of both collections are the same size and 
	 * the expected collection contains all the elements in the 
	 * collection which is being compared to it.
	 */
	public static boolean collectionsSame(	Collection<? extends Object> expected,
											Collection<? extends Object> actual)
	{
		boolean result = expected.size()==actual.size() &&
							expected.containsAll(actual);
		return result;
	}


	/*
	 * Tests that the given object is serializable, and that 
	 * object state is not affected by serialization. Note that 
	 * this method relies on the Object's equals(...) method 
	 * for comparison.
	 */
	public static void 	testSerialization(Object fixture) 
						throws IOException, ClassNotFoundException
	{
		ByteArrayOutputStream outputByteStream=null;
		ObjectOutputStream objectOutputStream=null;
		ByteArrayInputStream inputByteStream=null;
		ObjectInputStream objectInputStream = null;
		
		try
		{
			//write object
			outputByteStream = new ByteArrayOutputStream();
			objectOutputStream = new ObjectOutputStream(outputByteStream);
			objectOutputStream.writeObject(fixture);
			objectOutputStream.flush();
			objectOutputStream.close();		
			
			//read the object again
			byte[] objectRep = outputByteStream.toByteArray();
			inputByteStream = new ByteArrayInputStream(objectRep);
			objectInputStream = new ObjectInputStream(inputByteStream);
			Object readFixture = objectInputStream.readObject();
			objectInputStream.close();
			
			assertEquals(	"De-serialised object is not the same as serialised object.",
							fixture, readFixture);
		}
		finally
		{
			close(outputByteStream);
			close(objectOutputStream);
			close(inputByteStream);
			close(objectInputStream);
		}
	}
}